Charlie Calvert's C++ Builder Unleashed
- 19 -
Inheritance
This chapter focuses on object-oriented programming (OOP) as it applies to the
VCL. Specifically, it takes a close look at inheritance, one of the big-three topics
in object-oriented code. The other two key topics are encapsulation and polymorphism,
which you learn about in the next two chapters. Even if you already know all about
OOP, you should still at least skim this chapter so you can learn about the difference
between VCL objects and standard C++ objects.
In particular, this chapter covers the following topics:
- OOP theory and basics
- VCL object construction
- Inheritance
- Virtual methods
- Aggregation
- Form inheritance
The text focuses on several programs designed to show how objects are constructed.
One of the programs is developed in several stages so that you can see how an object
hierarchy emerges out of a set of raw ideas.
After you read the next three chapters on inheritance, encapsulation, and polymorphism,
the next big step is to learn how to build components. In fact, the real justification
for learning this material is that it gives you the ability to start creating your
own components. Building your own components is one of the most important tasks you
can tackle in BCB, so I will lay the groundwork for it carefully.
It is important to understand that the next three chapters are aimed at programmers
who want to work inside the VCL. I make no attempt to do justice to all the complex
features of the C++ object model. Instead, I try to present you with a subset of
those features as they apply to the VCL. This means that I make short shrift of interesting
topics such as function and operator overloading. My intent, however, is to show
you how to create components. You do not have to be an expert in C++ OOP theory to
achieve that goal.
For all its wonders, I don't think there is anything in C++Builder that even approaches
the significance of components. VCL components are the most amazing technological
achievement I have seen in contemporary programming. If you want to do something
really fantastic with your computer, then pay attention to the next few chapters
so that you can learn how to build great components.
When reading this chapter, you might want to make use of the ClassBrowser sample
program that ships with BCB. It allows you to explore the hierarchy of the VCL. This
program is found in the Examples/ClassBrw directory. It is far from perfect,
but it will serve to give you an overview of the VCL classes. You should also go
to www.object-domain.com and see whether
they have a version of Snorkle for C++Builder available. The versions of Snorkle
for Delphi that I have seen are very nice indeed, and if they can duplicate their
efforts in the world of C++, then most readers of this book will want to test their
technology.
About Objects
It might seem a little strange to start focusing on objects this late in the book.
After all, almost every program I have shown so far uses object-oriented code. So
how could I wait this long to begin talking seriously about objects? To answer this
question, I need to discuss two different issues:
- How does BCB treat objects?
- Why do people write object-oriented code?
The developers wanted BCB to be very easy to use. By its very nature, OOP is not
always a simple topic. As a result, BCB goes to considerable lengths to hide some
of the difficulties of object-oriented programming from the user. The biggest steps
in this direction include the automatic construction of Form1 as an object
and the existence of the delegation model. The fact that the scaffolding for most
methods is produced automatically by the IDE is one of the key ways the product saves
time--and one of the key ways it eases the process of producing applications.
The simple fact is that some people would never be able to approach BCB if they
had to go through the process of writing all this every time they created a form:
//--------------------------------------------------------------------------
#ifndef Unit1H
#define Unit1H
//--------------------------------------------------------------------------
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
//--------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:
private:
public: // User declarations
virtual __fastcall TForm1(TComponent* Owner);
};
//--------------------------------------------------------------------------
extern TForm1 *Form1;
//--------------------------------------------------------------------------
#endif
I'm leaving out the implementation of the constructor, and a few other features,
but in a stripped-down form, this code is indeed the basis for most BCB units. It's
simple enough to write; nonetheless, it could form a barrier between the product
and certain types of programmers.
The next obvious question is, "Why did the developers choose to write object-oriented
code if the subject itself can at times become somewhat complex? Why not just use
the relatively simpler framework provided by structured programming?" The answer
is that although it is simpler to create small structured programs than small object-oriented
programs, it's easier to write large object-oriented programs than it is to write
large structured programs.
OOP brings discipline and structure to a project. In the long run, this makes
coding easier. The problem is the learning curve associated with understanding OOP.
Almost everyone agrees that it's easier to finish a group project if you appoint
a leader for the group; it's easier to win at sports if you practice regularly; and,
ultimately, it's easier to become a good musician if you sit through some boring
lessons with a professional. It also might seem at first as if structured programs
are simpler to learn how to write and, therefore, are simpler to write, but this
isn't true. Just as it helps to take lessons, practice, and learn discipline if you
want to become good at playing a sport or a musical instrument, it helps to learn
object- oriented code if you want to write good programs.
Here's another way of stating the same matter. There is nothing you can do with
object- oriented code that you can't also do with structured programming. It's just
that OOP makes it relatively easy to construct programs that are fundamentally sound
and easily maintained. This doesn't mean you can't write structured programs that
are every bit as architecturally sound as object-oriented programs. The problem,
however, is that it is very difficult to design a structured program that is truly
modularized and truly easy to maintain. Object-oriented code, on the other hand,
has a natural tendency to move you in the direction of a sound, well-structured design.
The thesis of this chapter is that object-oriented code is basically a technique
for designing robust, well-planned programs. The syntax of OOP emerged out of the
desire to help programmers design applications that work. It is perhaps arguable
as to whether or not OOP by itself succeeded in achieving its goal, though certainly
I personally believe that it is a success. However, I think it is undeniable that
OOP in conjunction with components is the answer to many core programming problems.
If your only experience with components is in creating ActiveX controls, then you
haven't yet seen what this technology can do. The combination of OOP and components
is something that can make programmers many times more productive than they had ever
imagined possible when writing structured code, or when working with either objects
or components alone.
NOTE: It's probably worth pointing out that OOP
is not a separate subject from structured programming but its natural child. OOP
emerged out of the same types of thinking that generated structured code. Much of
what is true in structured programs is also true in object-oriented programs, except
OOP takes these theories much further. Object-based programmers should know nearly
everything that structured programmers know and should then add another layer of
information on top of it.
OOP is certainly not the end-all and be-all of programming. Rather, it is an intermediate
step in an ongoing process that might never have an end. BCB, with its heavy use
of components, already shows part of what the future holds. In particular, the future
is about components and visual manipulation of objects.
The Object Inspector enables you to see inside objects and to start to manipulate
them visually. You can do this without having to write code. It is quite likely that
this trend will continue in the future, and you will start to see programs not as
code but as a series of objects depicted as a hierarchy. If programmers want to manipulate
these objects, they will be able to do so through tools such as the Object Inspector
or through other means currently being used only in experimental languages.
To take this out of the clouds for a moment, here is my list of what's best about
BCB:
- Visual design tools
- A component architecture replete with a delegation model
- A real object-oriented language
Here are the same ideas looked at again from a slightly more in-depth perspective:
- Visual Tools: You can easily design a form using visual tools. To create a useful
form, you want to be able to arrange and rearrange the elements of the visual design
quickly and easily. BCB excels at this.
- Components: You want to be able to manipulate objects not only as code, but as
seemingly physical entities you can handle with the mouse. Components provide an
ideal solution to this problem. For example, the plastic Lego sets you played with
as a child were fascinating because they enabled you to build complex structures
out of simple, easy-to-manipulate pieces. In other words, Legos let you concentrate
on the design of structures, making the actual construction of a robust and easy-to-maintain
building relatively trivial. Components give the same kind of flexibility.
- OOP: Objects, and particularly the ability to view object hierarchies in a browser,
make it easy to see the overall design of a program. It's possible to see how a program
is constructed not only by looking at the code, but also by looking at an object
hierarchy made up of reusable classes. Use the ClassBrowser example or a copy of
Snorkle to view these hierarchies as they appear in your own programs. These kinds
of abstract, visual representations of a code base aid in the process of designing
and maintaining a program. A key word here is reuse. You can write an object once
and then use it over and over again. Reusability is what OOP is all about.
OOP, then, is part of a theory of design that is moving increasingly in the direction
of reusable, visual components that can be manipulated with the mouse. Undoubtedly,
this means that some types of programs that are difficult to construct today will
become trivial to build in the future. BCB has already performed this magic with
databases. A 10-year-old child could use BCB to construct a simple database application.
However, creating complex programs will probably always be difficult, simply because
it is so hard to design a good program that performs anything more than trivial tasks.
First printing presses, then typewriters, and finally word processors have made writing
much easier than it used to be, but they have not succeeded in making us all into
a race of Shakespeares.
NOTE: One thing that is not built into
BCB that can help you create robust programs is a good object-modeling tool. There
are some tools, such as the products called WithClass and Snorkle, that are designed
to work with the Delphi VCL and should soon appear in a BCB-based format.
It is worth pointing out that it is easy to draw object hierarchies using some form
of custom or agreed upon notation. Programs such as Visio or Playground can help
with this process. Even if there is no direct code generation involved, there is
still an enormous benefit to be derived from this process.
I have worked on projects I thought had gone hopelessly astray and could not ever
be salvaged. These "lost causes" were saved by simply drawing out my object
hierarchy with a tool that would let me rearrange its elements in several different
patterns. C++ is a great language, but it offers no means for providing an overview
of your object hierarchy. Drawing the object hierarchy with a simple object notation
can help enormously when it is not clear how to design a particular feature or when
you need to try to salvage a product that has gone astray.
BCB's object-oriented, component-based architecture makes programming easier than
it used to be. That doesn't mean that now everyone will be able to program. It just
means that now the best programmers can make better applications. The key terms are
reuse, visual design tools, components, and objects. If you can find an object-modeling
tool that can aid in program development, you will be even further ahead.
Creating Simple Objects
To start a discussion of objects, it might be a good idea to cut the VCL out of
the picture as much as possible. This will eliminate the complex object hierarchy
associated with the VCL. In its place, you can construct some very simple objects
with a known hierarchy that is easy to define. As the discussion progresses, the
VCL can be introduced into the programs in a planned and sensible manner.
- 1. Start a new project.
2. Bring up the Project Manager from the View menu and remove Form1.cpp
and the project resource file.
3. Go to the View menu again and choose Project Source.
4. Go to Options | Project | Linker and choose Console application, as shown
in Figure 19.1.
FIGURE
19.1. Creating a console application in
BCB.
Edit the main source file for the project so it looks like this:
#include <stdio.h>
int main(void)
{
printf("Daughters of Time, the hypocritic Days,\n");
printf("Muffled and dumb like barefoot dervishes\n");
printf("-- Ralph Waldo Emerson");
return 0;
}
Save this file as Object1.mak. It is now a complete application that
circumvents the VCL. If you open up a DOS window and run the program from the DOS
prompt, the output looks like Figure 19.2.
FIGURE
19.2. The output from the first take of
the OBJECT1 program as it appears when run from the DOS prompt.
It might seem strange to you that I have gone out of my way to eliminate so much
of the object hierarchy in a chapter that is about objects. My goal, however, is
to clear the boards so that you can view objects in a simplified state, thereby clearly
delineating their most salient points.
The program that unfolds through the next few pages is called OBJECT1. This is
a very simple object-oriented program that you will build on the console application
framework established earlier. I'm not going to start by showing you the code for
the whole program, because I want you to build it one step at a time so that its
structure emerges little by little.
To begin, you should create a small object at the top of the program:
class TMyObject
{
};
int main(void)
{
return 0;
}
All I have done here is added a simple class definition and removed the printf()statements.
Delphi programmers should note that this class is not a descendant of TObject,
even though it would have been in Object Pascal. One of the fundamental rules of
Object Pascal programming is that it is impossible to build an object that is not
a descendant of TObject or one of TObject's children. The reason
for this rule is that TObject contains some RTTI-based intelligence that
is needed by all BCB objects. This same intelligence is present in the metaclass
that is part of the BCB version of TObject. However, you can create C++
objects that do not descend from TObject and thus do not include this intelligence.
NOTE: You will find that I use the words
class and object almost completely interchangeably. This is technically correct,
although there is some merit in using the word class to describe the written declarations
that appear in a text file and object to refer to a compiled class that is part of
a binary file. In other words, programs are made up of objects, whereas source files
show class definitions. However, this distinction is not one that I spend a great
deal of time stressing in this book.
To create a true VCL object, you should change TMyObject's definition
so that it reads as follows:
#include <vcl\vcl.h>
class TMyObject : public TObject
{
public:
__fastcall TMyObject(void) : TObject() {};
}
int main(void)
{
return 0;
}
Logically, there is now a considerable difference between this declaration and
the one you created earlier. In particular, this is now a VCL object and must be
created on the heap. It also supports VCL specific syntax such as the __published
directive.
All VCL objects must have a constructor, and it should be declared __fastcall.
Methods or functions declared __fastcall can have some of their parameters
passed in registers, rather than always being pushed on the stack. This is the calling
convention used by VCL constructors, so you should conform to it.
All VCL objects that are descendants of TComponent must have destructors
declared __fastcall virtual:
__fastcall virtual TComponent(TComponent* Aowner);
The reason for the __fastcall and virtual restrictions has to
do with conformance to the VCL programming model. In particular, the VCL declares
the constructor for TComponent as virtual, so C++ objects that
descend from TComponent must follow along. This means that all components
you create must be declared with virtual constructors, because all components
are, at least in practice, descendants of TComponent. I comment on this
fact simply because it is very unusual for C++ constructors to be declared virtual.
NOTE: Actually, it is theoretically possible
for you to create your own class that performs the same chores that TComponent
performs. There is nothing magical about TComponent; it simply contains
standard VCL code that makes it possible for an object to live on the Component Palette.
It is not practical to duplicate this effort in your own code, and so one could perhaps
go so far as to say that "by definition" all components are descendants
of TComponent. However, this is not strictly true, as you could create your
own class that appears on the Component Palette without descending from TComponent.
I personally cannot imagine any set of circumstances that would justify the effort
involved in duplicating the work done in TComponent.
The declaration and implementation for a C++ constructor can have two forms. They
can appear entirely inside a class declaration, or you can split them up, with the
declaration inside the class and the implementation outside:
#include <vcl\vcl.h>
class TMyObject : public TObject
{
public:
__fastcall TMyObject(void);
};
__fastcall TMyObject::TMyObject(void) : TObject()
{
}
int main(void)
{
return 0;
}
When you implement a constructor, you should follow the header with a colon and
a call to the ancestor's constructor:
_fastcall TMyObject::TMyObject(void) : TObject()
Now that you have an overview of a basic VCL object declaration, the next step
is to declare a variable of type TMyObject and then instantiate it and dispose
of it:
class TMyObject : public TObject
{
public:
__fastcall TMyObject() : TObject() {}
};
int main(void)
{
TMyObject *MyObject = new TMyObject;
delete MyObject;
return 0;
}
The code shown here doesn't do anything functional. Its only purpose is to teach
you how objects work. Specifically, it declares a variable of type TMyObject:
TMyObject *MyObject
Next, it allocates the memory for the object:
new TMyObject;
Put together on one line, the statement looks like this:
TMyObject *MyObject = new TMyObject;
This statement actually creates a pointer variable of type TMyObject.
In VCL programming, you have to take this step if you want to use MyObject,
and, furthermore, you must dispose of this memory when you are finished with it.
There are two ways to destroy an object. One is to call Free, and the
other is to use the delete operator:
MyObject->Free();
delete MyObject;
Both techniques have the same outcome. I believe the majority of people prefer
delete, and it is what I use most often in the code found in this book.
However, there are reasons you might want to call Free, so I will discuss
it in the next few paragraphs.
When you free an object, what you are really doing is calling the object's destructor.
The following code shows approximately what takes place in the Free method
of TObject:
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
The variable Self always points to the current object. It plays the same
role in Object Pascal that this plays in C++. If you are inside one of the
methods of an object, you can refer to that object by using Self. (Self
is passed as an implicit parameter to all BCB methods.) Here is how the VCL Free
method would look in C++:
void __fastcall TObject::Free()
{
if (this != NULL)
~TObject();
}
NOTE: Programmers use the words descendant,
child object, derived class, and subclass as synonyms. I prefer to use either descendant
or child object, because subclass is also used in another context and derived class
seems unnecessarily obscure. My feeling is that it's best to stick to one metaphor:
parent, child, ancestor, and descendant, where child and descendant are synonymous,
and parent and ancestor are synonymous.
Standard C++ does not define a Free method. This is something specific
to the VCL. It is added to the VCL to make objects easier to use. It is very bad
to call the destructor of an object that no longer exists. As a result, the Free
method is there to provide a check that gives you some measure of protection against
this error. Despite this, the general consensus is that it is best to call delete.
The great virtue of delete is that it looks like standard C++ code, and C++ programmers
care a lot about standards.
Now that you know how to declare, allocate, and deallocate a simple object, it's
time to narrow the focus and tackle the subject of inheritance. The next two sections
are dedicated to this chore--specifically, to explaining the relationship between
a parent and child object.
Understanding Inheritance
In general, a child object can use any of its parent's methods. A descendant of
an object gets the benefit of its parent's capabilities, plus any new capabilities
it might bring to the table. I say that this is true in general, because the private
directive can limit the capability of a child to call some of its parent's routines.
The private directive is explained in depth later in this chapter.
Except for its constructor, all of TMyObject's methods and fields are
inherited:
class TMyObject : public TObject
{
public:
__fastcall TMyObject() : TObject() {}
};
This declaration is somewhat deceiving because TObject contains many
methods that are available to instances of TMyObject. In other words, TMyObject
is not quite as simple an object as it appears at first.
So, what are all these methods associated with TObject? Well, you can
see their definitions, as well as their implementations, if you open up the SysDefs.h
file from the \BCB\Include\VCL subdirectory:
class __declspec(delphiclass) TObject
{
public:
__fastcall TObject() {}
__fastcall Free();
TClass __fastcall ClassType();
void __fastcall CleanupInstance();
void * __fastcall FieldAddress(const ShortString &Name);
static TObject * __fastcall InitInstance(TClass cls, void *instance);
static ShortString __fastcall ClassName(TClass cls);
static bool __fastcall ClassNameIs(TClass cls, const AnsiString string);
static TClass __fastcall ClassParent(TClass cls);
static void * __fastcall ClassInfo(TClass cls);
static long __fastcall InstanceSize(TClass cls);
static bool __fastcall InheritsFrom(TClass cls, TClass aClass);
static void * __fastcall MethodAddress(TClass cls, const ShortString &Name);
static ShortString __fastcall MethodName(TClass cls, void *Address);
...// Code omitted here
virtual void __fastcall Dispatch(void *Message);
virtual void __fastcall DefaultHandler(void* Message);
private:
virtual TObject* __fastcall NewInstance(TClass cls);
public:
virtual void __fastcall FreeInstance();
virtual __fastcall ~TObject() {}
};
You can find the entire implementation of TObject in the Object Pascal
System.pas unit that ships with BCB. Much of it is actually in assembler,
but the source is there if you want to study it. You should, of course, also examine
the declaration for TObject in SysDefs.h. The calls in SysDefs.h,
however, ultimately resolve into calls to the Pascal implementation in System.pas.
I should perhaps add that the TObject declaration in SysDefs.h
is very hard to understand, but I promise you that it does end up resolving into
calls that access the System.pas version of TObject.
NOTE: Although I have mentioned this subject
before, it's probably once again time to stress the importance of viewing the Pascal
source code to the VCL. Your version of BCB might or might not ship with the source,
but if you don't have it and can possibly afford to buy it, you should think seriously
about obtaining it. You should peruse the BCB\Include\VCL subdirectory that
contains the header files for the imported VCL Pascal units. These files provide
the interface for key BCB units. They are not as good as having the source, but they
are very valuable. I refer to both the header files and the Pascal source continuously.
You can see that TObject has a few basic functions declared right at
the top:
__fastcall TObject() {}
__fastcall Free();
virtual __fastcall ~TObject() {}
The point to grasp here is that TMyObject has a destructor and Free
method because it inherits them from TObject.
To understand this point, you can add a line of code to the nascent OBJECT1 program:
#include <conio.h>
#include <stdio.h>
int main(void)
{
TMyObject *MyObject = new TMyObject;
AnsiString S = MyObject->ClassName();
printf(S.c_str());
delete MyObject;
getch();
return 0;
}
This code enables the object to write its name to the screen. The output from
this program is a single string:
TMyObject
When you run the program, this string might flash by too quickly for leisurely
perusal. To remedy the situation, add a getch() at the very end of the code,
right before the return statement. To end this program, press Enter. (That's
the way it used to be done back in the DOS world.)
If you want to, you can even get this object to say its parent's name:
int main(void)
{
TMyObject *MyObject = new TMyObject;
printf(Format("ClassName: %s\nParent's ClassName: %s",
OPENARRAY(TVarRec, (
MyObject->ClassName(),
MyObject->ClassParent()->ClassName()))).c_str());
delete MyObject;
getch();
return 0;
}
The output from this code is the following:
ClassName: TMyObject
Parent's ClassName: TObject
The point, of course, is that TMyObject inherits quite a bit of functionality
from its parent, and, as a result, it has numerous capabilities that might not be
obvious from merely viewing its declaration.
The ability to trace an object's ancestry is relatively appealing, so it might
be nice to add it to TMyObject as a method:
#include <vcl/vcl.h>
#include <stdio.h>
#include <conio.h>
#include "classrefs.h"
USEUNIT("ClassRefs.cpp");
class TMyObject : public TObject
{
public:
TMyObject() : TObject() {}
void PrintString(AnsiString S);
void ShowHierarchy();
};
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
int main(void)
{
ShowClassReferences();
TMyObject *MyObject = new TMyObject;
MyObject->ShowHierarchy();
delete MyObject;
getch();
return 0;
}
This version of the OBJECT1 program includes two methods, listed in the TMyObject
class declaration:
class TMyObject : public TObject
{
public:
TMyObject() : TObject() {}
void PrintString(AnsiString S);
void ShowHierarchy();
};
Take a look at the implementation for ShowHierarchy. Perhaps the first
thing you notice in it is the class reference in the first line, which uses the TClass
type.
The type TClass is an object reference and is declared in Sysdefs.h
as follows:
typedef TMetaClass* TClass;
Because ClassParent returns a variable of type TClass, it is
obviously what needs to be used here.
NOTE: An object reference is a special
metaclass that can be assigned to an object. Here is a unit that shows some legal
uses of an object reference:
///////////////////////////////////////
// ClassRefs.cpp
// Object1
// copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <forms.hpp>
#pragma hdrstop
#include "ClassRefs.h"
TClass AClass;
class TDescendant: public TObject
{
public:
TDescendant(): TObject() {}
};
void ShowClassReferences()
{
printf("** Start object references **\n");
AClass = __classid(TObject);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
AClass = __classid(TDescendant);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
AClass = __classid(TForm);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
printf("** End object references **\n\n");
}
Notice that you do not have to create an object before you can use it with an
object reference. In general, you can call any of the static methods of
TObject with a class reference.
You cannot use an object reference to refer to a field that belongs only to the child
of the object reference type. For instance, this code does not compile because
Caption is not a property of TObject:
ObjectRef = __classid(TForm)
ObjectRef.Caption := `Sam';
WriteLn(ObjectRef.Caption);
You will find a version of the CLSREF unit in the same subdirectory as
OBJECT1. You can use the Project Manager to add this file to the project, and you
can then call it in the second line of the body of the OBJECT1 program. However,
you should not leave this unit as part of the project, because it will muddy the
view of the object hierarchy that you get in the Browser.
There are not that many times in which you need to use an object reference in day-to-day
programming. If you are not totally clear on what they do, you can probably afford
to skip the subject. If you really want to know more, you should examine Sysdefs.h;
recognize that the TMetaClass you see there is the C++ way of creating a
feature that exists in the VCL. The reason the VCL supports this feature is that
it needs fairly extensive Run Time Type Information (RTTI) in order to run, and it
gets a good portion of that information from the methods in TObject that
are part of TMetaClass and are in turn used in an object reference.
When you get past the object reference, the remaining portions of the ShowHierarchy
method are fairly straightforward:
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
This code first writes the ClassName of the current object, which is
TMyObject. Then it gets the ClassParent, which is TObject,
and writes its name to the screen. The code then tries to get TObject's
parent and fails, because TObject has no parent. At this point, AClass
is set to NULL and the code exits the while loop. The output for
the program is shown in Figure 19.3.
FIGURE
19.3. The output from the OBJECT1 program.
In this section, you have learned about the Create, Destroy,
Free, ClassParent, and ClassName methods of TObject.
The declaration of TObject shows that several other methods are available
to BCB programmers. However, I do not discuss these methods in depth because they
are either self-explanatory (InheritsFrom) or beyond the scope of this book.
I should mention, however, that some of these routines are used by the compiler itself
when dispatching routines or performing other complex tasks that usually require
RTTI support. These are advanced programming issues that impact only a very small
percentage of BCB programmers.
Virtual Methods
Inheritance, in itself, is an interesting feature, but it would not take on much
significance were it not for the presence of virtual methods. Virtual methods can
be overridden in a descendant class. As such, they provide the key to polymorphism,
which is a trait of OOP programs that enables you to give the same command to two
different objects but have them respond in different ways. This chapter introduces
polymorphism, but I will leave the more complex aspects of this subject for Chapter
21, titled, appropriately enough, "Polymorphism." Polymorphism is a relatively
difficult subject to grasp; therefore, I have stretched out a full explanation of
it over several chapters.
Unlike Object Pascal, BCB has only one type of virtual method. This directive
tells the compiler to store the address of the function in a virtual method table.
NOTE: Delphi programmers should note that
C++ does not support either the dynamic or message directives. In their place, you
can use the MESSAGE_MAP macro.
The OBJECT2 program (shown in Listing 19.2) has one virtual method. The
virtual method is overridden in a child object. When you are creating the
OBJECT2 program, you should start with the source code for the OBJECT1 program. Modify
the code by declaring PrintString as virtual and by creating a
descendant of TMyObject called THierarchy. Also, don't forget to
make sure the program is set to work as a console application. If you don't have
this option checked, you can get an EInOutError exception. After changing
the setting, you should also rebuild your project so the new option takes effect.
NOTE: When creating one project based
on another, you can often copy the code from the directory where the first project
is stored into a separate directory made for the second project. After copying the
project, it is probably simplest to delete everything but the actual source files
from the new directory. For instance, delete the DSK file, the MAK file, and any
other extraneous files you will not need. Then create a new project, delete its main
form, and add copies of the source files you want to reuse from the previous project.
Otherwise, you might find paths hard-coded into your DSK or MAK files that address
files stored in the first program's directory. In this particular case, it is probably
easiest just to re-create the project entirely from scratch, but the information
in this note can be used as a general set of guidelines for use when copying projects
from one directory to another.
Listing 19.2. The source code
for the main unit in the OBJECT2 program.
///////////////////////////////////////
// Object2.cpp
// Project: Object2
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl/vcl.h>
#include <stdio.h>
#include <conio.h>
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
virtual void PrintString(AnsiString S);
};
class THierarchy: public TMyObject
{
int FColor;
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
__property int Color={read=FColor,write=FColor};
};
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
textcolor(FColor);
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
int main(void)
{
TMyObject *MyObject = new TMyObject();
MyObject->ShowHierarchy();
MyObject->Free();
THierarchy *Hierarchy = new THierarchy();
Hierarchy->Color = YELLOW;
Hierarchy->ShowHierarchy();
Hierarchy->Free();
getch();
return 0;
}
In OBJECT1, the ShowHierarchy method wrote its output to the screen.
Suppose that you found this object somewhere and liked the way it worked but wanted
to change its behavior so it could also write its output in color. The OBJECT2 program
shows a preliminary version of how you might proceed. After completing this first
take on creating a descendant of TMyObject, I will revisit the subject and
show ways to improve the model shown here. The output from the program is shown in
Figure 19.4.
FIGURE
19.4. The output from the OBJECT2 program.
In the old world of structured programming, the most likely step would be to rewrite
the original ShowHierarchy method. However, rewriting an existing method
can be a problem for two reasons:
- You might not have the source code to the routine, so you can't rewrite it. This
is a common problem because most programming tools are delivered in binary libraries.
- You might have the source code but also know that this particular method is already
being called by several different programmers. You, therefore, might be afraid to
rewrite it because you might break the other programmers' code. Furthermore, making
changes like this cuts you off from the upgrade path provided by the maker of the
library. You can't just use the maker's next version of the product out of the box,
because you now have a customized version of his or her library.
A combination of design and maintenance issues might deter the impulse to rewrite
the original method. Many projects have been delayed or mothballed because changes
in their designs have broken existing code and thrown the entire project into chaos.
OOP has a simple solution to this whole problem. Instead of declaring TMyObject
as
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
void PrintString(AnsiString S);
};
thoughtful programmers declare it like this:
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
virtual void PrintString(AnsiString S);
};
The difference is that in the second example, the PrintString method
is declared as virtual.
If PrintString is declared as virtual, you can override it in
a descendant object, thereby changing the way the method works without ever changing
the original version of the method. This means that all the other code that relies
on the first version of the program continues to work, and yet you can rewrite the
function for your own purposes. Furthermore, this technique would work even if you
didn't have the source code for TMyObject! I will show you how this works
in just a moment.
Some readers might have asked themselves earlier why I created the PrintString
method in the first place. The answer hinges on the fact that iterating through a
hierarchy of VCL objects can always be accomplished by the same algorithm. The same
technique works for all VCL objects. But the act of printing information to the screen
changes depending on your current circumstances. Are you in DOS? Are you in Windows?
Do you want to use colors? Each of these circumstances calls for a different way
of printing information to the screen. As a result, I separated the screen IO portion
of TMyObject from the portion of the object that iterates through a hierarchy.
Furthermore, there is no need to declare ShowHierarchy as virtual,
but I must declare PrintString as virtual. The reasoning here is
simply that ShowHierarchy does not need to change in descendants of the
object, but PrintString will need to change. In particular, it will need
to change so that it can write output in color. These types of considerations are
part of a subject known as object design.
At this point, you might think that using virtual methods seems like an unnecessarily
opaque solution to this problem. Wouldn't it have been simpler to add new methods
to the inherited class? Then the user of this second class could call these new methods
rather than the ones from the first instance of the class. There are three problems
with this technique:
- It requires the user to memorize a whole slew of different method names that
perform related but slightly different tasks. This is precisely what you have to
do in structured programming, and it is exactly what you want to avoid. Instead,
you want to have one name that applies to all similar methods of this family. For
instance, if you have an animal object, you are going to have to make the
Walk method virtual because a bird walks on two legs, and a cat
walks on four. The implementation of Walk is different for each animal,
so you need to declare the method virtual. When you are done, you can use
Cat->Walk(), and the cat will walk properly. Conversely, you can use
Bird->Walk(), and the bird will walk properly. It would be a mess if
you had to use Bird->WalkOnTwoLegs() and Cat->WalkOnFourLegs().
This proliferation of similar but slightly different method names is exactly the
kind of structured programming fiasco that OOP was designed to avoid. Instead of
a bunch of similar names like WalkOnTwoLegs, CrawlOnYourBelly,
WalkOnFourLegs, and WalkOnOneHundredLegs, you want to have just
one word, such as Walk, that applies to a whole family of objects. In other
words, each object in the family will implement the walk method differently.
- The second problem is that these objects call the PrintString method
internally. If you did not declare the method as virtual, you would have
to figure out some way for the ShowHierarchy method to know whether it should
call the implementation of PrintString that is part of TMyObject,
or whether it should call the implementation that is part of THierarchy.
By declaring a method as virtual, this chore will be handled for you automatically.
If you create an instance of THierarchy, ShowHierarchy will call
THierarchy->PrintString automatically; and if you create an instance
of TMyObject, it will call TMyObject->PrintString. This is the
way OOP handles virtual methods, and it is one of the key concepts that makes this
system work.
- The final, and best, reason for doing things this way has to do with polymorphism.
As such, it may not be clear to all readers at this time, but I promise that it will
make sense after you have read the polymorphism chapter. You can declare a pointer
of type TMyObject that can be assigned to a pointer of type THierarchy.
When you then call TMyObject->PrintString(), the PrintString
method of THierarchy will be called even though the object instance is of
type TMyObject. This would not occur if PrintString were not declared
virtual. In fact, this behavior is what polymorphism is all about, and indeed,
it is one of the cornerstones of OOP programming.
The word virtual is inherited from one class to the next. If a base class declares
a method virtual, the descendants need not do so, because they will inherit
the virtual declaration for a particular method from the base class. Delphi
users take note, as this is the exact opposite of what happens in Object Pascal.
I should add that it is generally considered bad form not to repeat the declaration
in child objects, as you want to be sure the reader of your code can see at a glance
how it is structured.
Here is a look at a stripped-down descendant of TMyObject that overrides
the PrintString method:
class THierarchy: public TMyObject
{
int FColor;
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
__property int Color={read=FColor,write=FColor};
};
This declaration states that class THierarchy is a descendant of class
TMyObject and that it overrides PrintString.
NOTE: Delphi programmers beware! The Pascal
object model performs the same chore by using the override directive. That's
not the way C++ works. This is a major change between the new BCB code and the old
Object Pascal techniques.
A first take on the new version of the PrintString method looks like
this:
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
textcolor(FColor);
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
This code depends on functionality from Conio.h that allows you to print
strings to the screen in color.
You can see that a field called FColor has been added to this object:
THierarchy now contains not only procedures but also data. One of the key
aspects of class declarations is that they can contain both methods and data, so
that you can bring all the code related to the THierarchy object together
in one place. This is part of a concept called encapsulation, explained in the next
chapter.
NOTE: By convention, VCL objects declare
private data as starting with the letter F, which stands for field. This technique
is helpful, because it highlights the difference between an object's properties and
its data. A property is published to the world and does not begin with the letter
F. Private data is inaccessible to the rest of the world and begins with the letter
F.
When you run the OBJECT2 program, the following code is executed in its main body:
int main(void)
{
TMyObject *MyObject = new TMyObject();
MyObject->ShowHierarchy();
MyObject->Free();
THierarchy *Hierarchy = new THierarchy();
Hierarchy->Color = GREEN;
Hierarchy->ShowHierarchy();
Hierarchy->Free();
getch();
return 0;
}
This code creates an object of type THierarchy and then shows you how
to use the new functionality of the ShowHierarchy method. I also create
an instance of TMyObject so that you can compare the two classes demonstrated
so far in this chapter.
NOTE: I should perhaps mention that it
is more expensive to declare or call virtual methods than it is to call a static
method. As a result, you need to weigh the whole issue of whether you want to declare
a method to be virtual.
In my opinion, you should usually create objects that have the best possible design,
re-gardless of the amount of overhead they entail. Of course, it is possible to take
this theory too far, but the mere fact of adding a few virtual methods is
usually not the problem in bloated object hierarchies.
Besides space and performance, a second reason for not declaring an object virtual
is a desire to hide its implementation so you can change it later. There is usually
no point in declaring a private method virtual, because it can't be seen
by other objects, unless they are friends of the original object. (I will talk about
friend objects later in this chapter.) The great advantage of private methods is
that they can always be changed later to whatever degree you want, because other
objects usually cannot see them and cannot access them directly. As a result, you
may want to declare methods private, and non-virtual, so you can
change their implementation later on. Needless to say, I am referring specifically
to the act of changing the number of parameters these methods take.
Your users will, of course, complain if you take a method they occasionally want
to override and make it private and non-virtual. However, it is
sometimes better to listen to their complaints than to saddle them with a broken
object that cannot be fixed without breaking existing code.
In this section, you have learned about the virtual directive. This subject's
true significance won't be clear until you read about polymorphism. However, before
you tackle that subject, it's best to learn more about inheritance and encapsulation.
In particular, the next section of the chapter looks at more issues involving object
design.
Searching for the Right Design
It is almost impossible to find the right design for an object the first time
you write it. As a rule, the only way you can figure out the design for an object
is by creating it, discovering its limitations, and then making improvements to its
design. In short, object design is an iterative process.
There is no good way to step you through the process of discovering the correct
object design in a book, because the written word is by its nature static, and the
process I'm describing is dynamic. Furthermore, it's confusing to the reader to show
a series of poorly designed objects that are successively improved in each iteration.
The problem with this technique is that the reader keeps seeing the wrong way to
create an object and can easily pick up bad habits or fundamental misconceptions
about object design. Even worse, the reader tends to get frustrated with having to
unlearn the techniques they just acquired in the previous example that are now revealed
as being flawed.
To avoid the problems outlined in the preceding paragraph, I will simply show
you a second version of the THierarchy object and explain why it contains
changes to the original version of the object you saw in the last section. This process
does not tell you much about how I discovered the flaws in the object, but it will
show you what the flaws are and how I got around them. The main point to grasp is
that object design is an iterative process, and that the correct way to find the
flaws in an object is to implement them once as best you can, and then look for problems.
NOTE: There is a second school of thought
that states that you can find the correct design for an object before implementing
it. Proponents of this technique often suggest that a team be split in two, with
part of the members designing objects and the other part implementing them. I have
to confess that I've never actually discussed the results of this technique with
someone who has used it successfully, as all my experience has been with programmers
who use the iterative technique I discuss here. (Some of these programmers have also
tried the second school of programming, but it did not work for them.)
Of course, you should try to get an object right the first time, and you should use
high-level tools that help with design. However, you should also expect to have to
refine the objects you create through a perhaps lengthy, iterative process.
Furthermore, you should design your object defensively. That is, you should carefully
hide your implementation inside private methods and data because you will surely
have to change its design at a later time.
The first problem with the original version of the THierarchy object
became clear when I wanted to find a method to clear the screen. When doing so, I
needed to first set the text and background color to which I wanted the screen to
be cleared. As a result, I added new properties and new set methods to the object.
The new property let me add a background color, and the set methods let me change
the text and background colors at the same time I assigned them to the private data:
class THierarchy: public TMyObject
{
int FTextColor;
int FBackColor;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; textbackground(FBackColor); }
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
virtual void ClrScr();
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
The implementation of PrintString now looks like this:
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
Note that I have removed the code that changed the color of the text. This code
is no longer necessary as the color gets changed in the SetTextColor method.
The extremely straightforward implementation of ClrScr looks like this:
void THierarchy::ClrScr()
{
clrscr();
}
NOTE: C++ allows you to declare methods
inline, as shown by the SetTextColor and SetBackColor
method declarations. Conversely, you can also declare a method outside of the object,
as I do in the case of ClrScr. This latter technique is called an out_of_line
declaration, but that has such a ghastly ring in my ear that I refuse to use it.
You can, in fact, implement a method outside an object and make it inline by using
the inline directive:
inline void SetTextColor(int Color)
{
FTextColor = Color;
textcolor(FTextColor);
}
Inline methods usually execute faster than regular methods because they are placed
directly in your code and do not require the overhead associated with a function
call. Whether you implement them inside or outside of a class declaration, you should
declare only very small methods as inline, and they should not contain any
loops.
Inline methods are great, and I use them regularly. The only drawback I see to them
is that they can make object declarations difficult to read. In particular, the great
thing about an object declaration is that it can provide a summary of the functionality
of an object without asking you to wade through its implementation. Inline methods
implemented inside a class declaration detract from this feature because they clutter
up the landscape.
One possible workaround is to declare inline functions separately from the object
declaration by using the inline keyword. In fact, this is probably the ideal
solution, and it is only laziness that keeps me from using it at all times. I, for
one, would have been glad if the compiler enforced this rule.
The next change I made to THierarchy involved a desire to increase the
flexibility of the object. In particular, it would be great to be able to use this
object even if you do not descend from it directly. One way to do this is via multiple
inheritance, but that technology is not supported by VCL objects. I should perhaps
add that I am not particularly partial to multiple inheritance as a technology for
use in real-world applications.
A much simpler way to make the object more flexible is to pass the ShowHierarchy
method a copy of the object whose hierarchy you want to explore:
void TMyObject::ShowHierarchy(TObject *AnObject)
{
TClass AClass;
AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();
PrintString(AClassName);
AClass = AnObject->ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
The interesting thing about this change is that it occurs at the level of the
TMyObject implementation and declaration. In other words, it represents
changes not to THierarchy but to TMyObject. If this object were
of more earth-shaking import and if I had released TMyObject to the public,
I could not have made this sort of change because I would be breaking the code of
those who already called the ShowHierarchy method. There are, I suppose,
three things you can learn from this example:
- 1. Try not to publish any part of an object hierarchy until you are sure
you can live with its current public interface.
2. Hide your implementation. Avoid letting consumers of your objects directly
call one of the methods that involves a significant part of your implementation.
3. Object design is an art, not a science. There is no such thing as a perfect
object. All objects have flaws. Plan them out carefully ahead of time, bring them
to fruition by a lengthy iterative process, and then send them out into the world
with the understanding that your design is flawed by definition. Recognize that you
are going to have to support the objects you send into the world, because your users
are going to find flaws in them. It is the mark of an amateur to stonewall the consumer
of an object when he or she offers intelligent criticism of an object implementation.
Likewise, it is naïve for the consumer of an object to expect it to be perfect.
Perfection is too high a goal. Instead, shoot for high quality, and then demand that
object producers provide fixes when flaws are discovered. Fixes need not arrive immediately,
but they should appear in the next version of the product.
After making these changes, I decided that the object was ready to see the light
of day. As a result, I moved it into its own file called MyObject.cpp. I
then made a program called OBJECT3 that tests this new arrangement. The results of
this effort are shown on the CD-ROM that accompanies this book in a program called
OBJECT3. The code for this program is shown in Listings 19.3 through 19.5.
Listing 19.3. TMyObject and THierarchy
now reside in their own file, called MyObject.
///////////////////////////////////////
// MyObject.h
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MyObjectH
#define MyObjectH
#include <conio.h>
#include <vcl\stdctrls.hpp>
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy(TObject *AnObject);
virtual void PrintString(AnsiString S);
};
class THierarchy: public TMyObject
{
int FTextColor;
int FBackColor;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; textbackground(FBackColor); }
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
virtual void ClrScr();
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
#endif
Listing 19.4. The implementation
for TMyObject and THierarchy are shown here in MyObject.cpp.
///////////////////////////////////////
// MyObject.cpp
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "myobject.h"
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy(TObject *AnObject)
{
TClass AClass;
AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();
PrintString(AClassName);
AClass = AnObject->ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
void THierarchy::ClrScr()
{
clrscr();
}
Listing 19.5. The test program
for the MyObject unit.
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "myobject.h"
USEUNIT("MyObject.cpp");
void main()
{
THierarchy *H = new THierarchy();
H->TextColor = YELLOW;
H->BackColor = BLUE;
H->ClrScr();
H->ShowHierarchy(H);
delete H;
getch();
}
Notice that I include code in the test program that "uses" the MyObject
unit. I did not insert this code manually but instead used the program manager to
add the MyObject module to the project. This is the best way to proceed,
as it allows you to avoid editing the make file.
The program itself simply sets a background and text color for the output, then
clears the screen to those colors and shows the hierarchy to the user. The next-to-last
line in the program calls delete rather than Free. You can use
either technique, depending on the dictates of your taste and background.
Showing the Hierarchy of a VCL
Program
To really appreciate the new objects developed in the last section, you need to
add them to a regular BCB application. For instance, if you use them in a standard
Form1 class, here is the output you get:
TForm1
TForm
TScrollingWinControl
TWinControl
TControl
TComponent
TPersistent
TObject
This shows the whole hierarchy of class TForm1, starting with this,
moving to TForm, back to TScrollingWinControl, and so on, all the
way back to TObject.
As you know, VCL objects do not support multiple inheritance. As a result, I could
not add the functionality of THierarchy to TForm by letting the
object inherit it. This is fine with me, because I use multiple inheritance only
very reluctantly.
Instead of multiple inheritance, I prefer to use a technique called aggregation.
In aggregation, you add the object you want to use as a field of your current object
and then expose its methods either by publishing the whole object as a property,
or by wrapping the methods of the object inside methods of your current object. This
technique allows you to easily take precise control of the functionality from the
new object, and it tends to support clean, bug-free programming.
Before I show you how to use aggregation, I need to point out that it is not possible
to use THierarchy directly in a Windows program because THierarchy
uses Conio.h. The question then becomes, "Is THierarchy designed
in such a way that I can descend from it and change its functionality so that it
works with a Windows program?"
Well, part of the answer should be obvious. The key method that had to be virtual,
the one that had to be overridden to make this work, is PrintString. And
indeed, PrintString is declared virtual, so it is possible to change
THierarchy's stripes. In fact, you will find that I also override the setters
and the ClrScr method.
NOTE: You might feel that having to override
so many methods in order to change the output of this program is an excessive amount
of work when compared to the relatively simple task of rewriting the ShowHierarchy
method from scratch. Indeed, when looked at from this perspective, the whole task
of creating an object in this case seems fruitless. I readily confess that it would
indeed have been simpler to write three versions of a non-OOP method called ShowHierarchyBlackandWhite,
ShowHierarchyColor, and ShowHierarchyWindows. Here is a ShowHierarchy
method tailored for use with a VCL form-based program:
void TForm1::ShowHierarchy()
{
TClass AClass;
Memo1->Clear();
Memo1->Lines->Add(AnsiString(ClassName()));
AClass = ClassParent();
while (True)
{
AnsiString S = AnsiString(AClass->ClassName());
Memo1->Lines->Add(S);
if (AnsiString(AClass->ClassName()) == "TObject")
break;
AClass = AClass->ClassParent();
}
}
Clearly, this object is easier to write than the objects I have produced here.
The key reason I created an object like THierarchy is simply that it illustrates
how OOP works. In real life, objects often contain 20, 30, or even 50 methods. When
seen in that light, having to override three or four methods does not seem like such
a big chore. Furthermore, you can often use delegation to solve these kinds of problems.
Of course, this isn't real life but a book on programming. I have therefore intentionally
created a small object with few methods so that I can focus your attention solely
on virtual methods that need to be overridden. If I had cluttered up this
chapter with a big object, you never could have seen the trees for the forest, because
at least half the chapter would have involved an explanation of a huge object.
Furthermore, the ShowHierarchy method is the difficult method in this program.
It's easy to write the methods that need to be overridden, while some programmers
might have trouble creating ShowHierarchy. In this sense, even the rather
simple object I have created here does a good job of hiding complexity and promoting
bug-free reuse.
A final point about this subject is that OOP's main strength is not in producing
small code, nor is it true that objects are necessarily easier to create than equivalent
structured code. Rather, the strength of OOP is in letting you easily and safely
reuse premade objects. Now that these objects are completed, they can be reused easily
in any WinTel-based C++ program, whether it runs in Windows or from the command line.
OOP never makes sense if you think in terms of writing your whole program from scratch.
Instead, the advantage of OOP comes when you want to create programs by reusing premade
objects. OOP is about code reuse and about writing bug-free programs; it is not about
finding the smallest possible implementation of an algorithm. When you add components
to OOP, you can also get RAD, which is another major boost in productivity.
In Listings 19.6 through 19.9, you will find the code for the new version of the
Object1.cpp module, as well as the code for a standard VCL form-based program
called HIERARCHY that uses the object. The output from the program is shown in Figure
19.5.
FIGURE
19.5. The HIERARCHY form sports a TButton
and a TMemo component.
Listing 19.6. The new code for
the MyObject header file.
///////////////////////////////////////
// MyObject.h
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MyObjectH
#define MyObjectH
#include <conio.h>
#include <vcl\stdctrls.hpp>
class TMyObject :public TObject
{
protected:
virtual void PrintString(AnsiString S);
public:
TMyObject() : TObject() {}
void ShowHierarchy(TObject *AnObject);
};
class TListHierarchy: public TMyObject
{
private:
TStringList *FList;
protected:
virtual void PrintString(AnsiString S)
{ FList->Add(S); }
public:
TListHierarchy() { FList = new TStringList(); }
__fastcall virtual ~TListHierarchy() { delete FList; }
TStringList *GetHierarchy(TObject *AnObject)
{ FList->Clear(); ShowHierarchy(AnObject); return FList; }
};
class __declspec(delphiclass) TVCLHierarchy;
class THierarchy: public TMyObject
{
friend TVCLHierarchy;
int FTextColor;
int FBackColor;
protected:
virtual __fastcall void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual __fastcall void SetBackColor(int Color)
{ FBackColor = Color; textbackground(FBackColor); }
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
virtual void ClrScr();
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
class TVCLHierarchy : public THierarchy
{
TMemo *FMemo;
protected:
virtual __fastcall void SetTextColor(int Color)
{ FTextColor = Color; FMemo->Font->Color = TColor(FTextColor); }
virtual __fastcall void SetBackColor(int Color);
public:
TVCLHierarchy(TMemo *AMemo): THierarchy() { FMemo = AMemo; }
virtual void PrintString(AnsiString S)
{ FMemo->Lines->Add(S); }
virtual void ClrScr() { FMemo->Clear(); }
};
#endif
Listing 19.7. The new code for
the MyObject implementation.
///////////////////////////////////////
// MyObject.cpp
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "myobject.h"
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy(TObject *AnObject)
{
TClass AClass;
AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();
PrintString(AClassName);
AClass = AnObject->ClassParent();
while (True)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
if (AClassName == "TObject")
break;
AClass = AClass->ClassParent();
}
}
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
void THierarchy::ClrScr()
{
clrscr();
}
void __fastcall TVCLHierarchy::SetBackColor(int Color)
{
FBackColor = Color;
FMemo->Color = TColor(FBackColor);
}
Listing 19.8. The code for the
HIERARCHY header file.
///////////////////////////////////////
// Main.h
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "MyObject.h"
class TForm1 : public TForm
{
__published:
TButton *ShowHierarchyBtn;
TMemo *Memo1;
TPanel *Panel1;
void __fastcall ShowHierarchyBtnClick(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall TForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern TForm1 *Form1;
#endif
Listing 19.9. The code for the
HIERARCHY program.
///////////////////////////////////////
// Main.cpp
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete Hierarchy;
}
void TForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
When you run this program, it will display the hierarchy of the TForm
object in a memo control. It does so by creating an aggregate of the THierarchy
object and the TForm1 object.
Creating Friend Objects
Here is the declaration for the descendant of THierarchy that works in
Windows:
class TVCLHierarchy : public THierarchy
{
TMemo *FMemo;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; FMemo->Font->Color = TColor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; FMemo->Color = TColor(FBackColor); }
public:
TVCLHierarchy(TMemo *AMemo): THierarchy() { FMemo = AMemo; }
virtual void PrintString(AnsiString S)
{ FMemo->Lines->Add(S); }
virtual void ClrScr() { FMemo->Clear(); }
};
Several changes to this program should jump right out at you. First, notice that
the constructor now takes a parameter. This parameter is the memo control that will
be used to display text to the user. The SetTextColor, SetBackColor,
PrintString, and ClrScr methods have all been rewritten to take
advantage of this new control.
As a result, the only parts of the original object that have come through unchanged
are as shown in the following pseudocode:
class THierarchy: public TMyObject}
{
int FTextColor;
int FBackColor;
public:
void ShowHierarchy(TObject *AnObject); // inherited from TMyObject
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
In this particular case, it is not possible to use the TextColor and
BackColor properties to access the private FTextColor and FBackColor
data stores. Instead, I declare TVCLHierarchy to be a friend of THierarchy:
class TVCLHierarchy;
class THierarchy: public TMyObject
{
friend TVCLHierarchy;
int FTextColor;
int FBackColor;
... // Code omitted here
}
Notice that TVCLHierarchy is declared as a friend in the first line of
the THierarchy declaration. This means that TVCLHierarchy has access
to the private data of THierarchy. Descendants of TVCLHierarchy
will not inherit this trait.
Using Aggregation
Rather than use multiple inheritance to access TVCLHierarchy, TForm1
declares a field of this type:
class TForm1 : public TForm
{
... // Code omitted here
TVCLHierarchy *Hierarchy;
void ShowHierarchy();
public:
virtual __fastcall TForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
I have also added a method called ShowHierarchy and a new property called
Hierarchy so that descendants of TForm1 can have access to the
BackColor and TextColor properties of TVCLHierarchy.
A fine point of object design could be debated here. In this particular case,
I have created a property of type TVCLHierarchy that is exposed to consumers
of TForm1. Alternatively, I could have completely hidden TVCLHierarchy
behind a set of properties that provided access to the FTextColor and FBackColor
fields of THierarchy:
class TForm1 : public TForm
{
... // Code omitted here
TVCLHierarchy *Hierarchy;
void ShowHierarchy(TObject *Object);
int GetHierarchyBackColor();
void SetHierarchyBackColor(int Color);
int GetHierarchyTextColor();
void SetHierarchyTextColor(int Color);
public:
virtual __fastcall TForm1(TComponent* Owner);
__property int HierarchyTextColor=
{read=GetHierarchyTextColor, write=SetHierarchyTextColor};
__property int HierarchyBackColor=
{read=GetHierarchyBackColor, write=SetHierarchyBackColor};
};
This technique is very pure from an OOP-based point of view, and it is perhaps
a more classic and complete example of aggregation than the one I use. However, it
is not strictly necessary to create the HierarchyTextColor and HierarchyBackColor
properties. Instead, I can make the Hierarchy object public by making it
a property of type TVCLHierarchy. The reason this approach is acceptable
is because FHierarchy, as well as the FTextColor and FBackColor
fields of THierarchy, are all still hidden behind properties.
It would be wrong, however, to make FHierarchy public or to allow users
to directly access the FTextColor or FBackColor fields of THierarchy.
The reason this is wrong is because it exposes your data to the public, thereby limiting
your choices when it comes time to maintain or improve your program.
The technique shown here of exposing TVCLHierarchy via a property shows
up everywhere in the VCL. Most notably, it is present in the Items or Lines
fields of TListBox, TComboBox, and TMemo. The point here
is that you want to use properties to protect your data and your implementation,
but you don't want to take things so far that your code becomes needlessly bloated.
However, I do not feel that it would be incorrect or wrong to completely hide
FHierarchy from consumers of TForm1 and to instead expose its properties
via properties of TForm1. The great advantage of this technique would be
that it would allow a seamless aggregation of TForm1 and TVCLHierarchy.
NOTE: If you sense me waffling a bit here,
that is because I do not believe there is a hard and fast answer as to what is best
in a situation like this. It happens that there are good arguments on both sides.
Component design is an art, not a science. The moment it becomes a science, most
programmers are going to be out of a job.
In the constructor for TForm1, you should create the TVCLHierarchy
object:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
As you can see, I have chosen to hard code default values for the BackColor
and TextColor properties of TVCLHierarchy into this constructor.
This is not necessarily the best thing to do from the point of view of a descendant
of TForm1, but it will not cause any problems in the simple example I am
creating here.
At last, I am able to create a wrapper for the ShowHierarchy method:
void TForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
Once again, you could probably just as easily let consumers of TForm1
access this method via the Hierarchy property. However, I provide this method
in part because it is easier to use than a property, and in part because I want to
illustrate explicitly how aggregation works.
Finally, I have included a method of TForm1 that calls the TForm1::ShowHierarchy
method:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
This method does nothing to forward my example of aggregation, and I include it
solely for expediency's sake. A real-world example of aggregation would leave TForm1
without features of this type and would instead expect you to inherit from it in
order to access the functionality exposed via Hierarchy and ShowHierarchy.
For an example of this type of program, see the HIERARCHY2 program in the Chap19
directory.
A reader could object that rather than using aggregation, I could have declared
a single method that looked like this:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
THierarchy *Hierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
Hierarchy->ShowHierarchy();
delete Hierarchy;
}
Once again, I have to confess that this probably is a simpler technique in this
particular case. However, if you created descendants of TForm1, aggregation
would allow them to call the ShowHierarchy method with one line of code.
It would, in effect, add the functionality of ShowHierarchy to TForm,
so that TForm1 appeared to be an "aggregate" of two objects. Admittedly,
the simplicity of the example I show here makes these advantages difficult to discern,
but when you are working with larger, more complex objects, this technology really
starts to shine. OOP is about managing complexity, but it is easier to teach OOP
if you use simple examples.
By this time, you should have a fairly good grasp of inheritance and hierarchies.
The key point to understand is that, in general, a child object inherits the capability
to use any of its parent's methods. In other words, it comes into an inheritance,
where the inheritance is the methods, fields, and properties of its parent. Except
for TObject itself, all VCL objects have parents that can trace their roots
back to TObject.
Form Inheritance and Aggregation
Form inheritance is a powerful technique used by VCL programs. Here is a description
of how to use it.
- 1. In the HIERARCHY2 program, I remove the ShowHierarchy button
from the Form1 file used in the HIERARCHY program.
2. I also rename Form1 to HierarchyForm1. I then save the old
Form1 file as a new file called HierarchyForm.
3. Next I start a new project and remove its main form.
4. I add the HierarchyForm to this new project and use the Object
Repository to create a new form that descends from HierarchyForm1. To do
this, choose File | New, click on the HIERARCHY2 page in the Object Repository, and
choose to inherit a new form from HierarchyForm1.
5. Save the new form you have created as Main.cpp.
6. Open the Options | Project menu and use the Forms page to set Form1
as the main form for your application. In other words, make the descendant of the
HierarchyForm the main form for your application.
When you are done, the declaration for your main form should look like this:
///////////////////////////////////////
// HierarchyForm.h
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef HierarchyFormH
#define HierarchyFormH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "Myobject.h"
class THierarchyForm1 : public TForm
{
published:
TMemo *Memo1;
TPanel *Panel1;
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
protected:
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall THierarchyForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern THierarchyForm1 *HierarchyForm1;
#endif
Go into the declaration for HierarchyForm1 and make sure that ShowHierarchy
is declared in the public or protected section.
Now add a button to the main form called ShowHierarchy and add the following
event handler to its OnClick event:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
Now run the program, and when you click on ShowHierarchy, you should
get the following result:
TForm1
THierarchyForm1
TForm
TScrollingWinControl
TWinControl
TControl
TComponent
TPersistent
TObject
As you can see, the hierarchy now goes from TForm1 to HierarchyForm1
to TForm, and so on. TForm1 is descended from THierarchyForm1,
and it therefore inherits the ability to all the things a regular form can do, plus
it can show its own hierarchy.
Once again, there are a number of quibbles you could make here. For instance,
it might be better to remove the visual controls from the HierarchyForm,
and so on. However, the main point of this exercise is merely to illustrate how form
inheritance works, and the code shown here does that.
For the sake of clarity, I have included the complete code to the program in Listings
19.10 through 19.14. Remember that you need to use the visual tools to inherit from
the HierarchyForm, or Form1 will not have the correct appearance.
Listing 19.10. The header for the
HierarchyForm from the HIERARCHY2 program.
///////////////////////////////////////
// HierarchyForm.h
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef HierarchyFormH
#define HierarchyFormH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "Myobject.h"
class THierarchyForm1 : public TForm
{
__published:
TMemo *Memo1;
TPanel *Panel1;
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
protected:
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall THierarchyForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern THierarchyForm1 *HierarchyForm1;
#endif
Listing 19.11. The main form
for the HierarchyForm.
///////////////////////////////////////
// HierarchyForm.cpp
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "HierarchyForm.h"
#pragma resource "*.dfm"
THierarchyForm1 *HierarchyForm1;
__fastcall THierarchyForm1::THierarchyForm1(TComponent* Owner)
: TForm(Owner)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
void __fastcall THierarchyForm1::FormDestroy(TObject *Sender)
{
delete Hierarchy;
}
void THierarchyForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
Listing 19.12. The header for
the programs main form.
///////////////////////////////////////
// Main.h
// Hierarchy2
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "Myobject.h"
#include "HierarchyForm.h"
class TForm1 : public THierarchyForm1
{
__published:
TButton *ShowHierarchyBtn;
void __fastcall ShowHierarchyBtnClick(TObject *Sender);
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 19.13. The main form
for the program.
///////////////////////////////////////
// Main.cpp
// Hierarchy2
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "HierarchyForm.h"
#pragma link "HierarchyForm"
#pragma link "HierarchyForm"
#pragma resource "*.dfm"
TForm1 *Form1;
//--------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: THierarchyForm1(Owner)
{
}
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
//--------------------------------------------------------------------------
Listing 19.14. The project source
for the HIERARCHY2 program.
#include <vcl\vcl.h>
#pragma hdrstop
USERES("Hierarchy2.res");
USEFORM("Main.cpp", Form1);
USEFORM("HierarchyForm.cpp", HierarchyForm1);
USEUNIT("..\..\Utils\MyObject.cpp");
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->CreateForm(__classid(THierarchyForm1), &HierarchyForm1);
Application->Run();
}
catch (Exception &exception)
{
MessageBox(NULL, exception.Message.c_str(), Application->Title.c_str(),
MB_OK);
}
return 0;
}
I have included the complete source for this program so you can double-check your
work against it in case anything goes wrong. The key parts you need to concentrate
on are the class declarations in the headers, the declarations for the global variable
for the HierarchyForm, and the headers for the various methods used in the
forms. The actual implementation of the methods was already cleared up in the HIERARCHY
program, so you don't need to check them. The HIERARCHY2 program is shown in Figure
19.6.
FIGURE
19.6. The HIERARCHY2 program at runtime.
You should spend some time adding controls to the HierarchyForm and watching
how they then automatically appear on the main form for the program. Notice that
if you move a control on the HierarchyForm, the corresponding control on
the main form will also move. If you move a control on the main form, however, that
breaks the connection between that control and the corresponding control on the HierarchyForm.
To restore the connection, right-click on the control in the main form and choose
Revert To Inherited from the menu.
You can disconnect and revert properties one at a time if you wish. For instance,
if you move the left side of a component on the main form, that will break the connection
for that one property, but it will leave the remaining properties still connected
to the HierarchyForm. To restore the Left property, choose that
property in the Object Inspector with the right mouse button and select Revert To
Inherited from the menu. To test this, you might want to drop a single button in
the middle of the HierarchyForm, then switch back to the main form and work
on changing just one of the inherited button's properties, such as its Left
property or its Caption.
Summary
In this chapter, you have had a good chance to start working with object-oriented
programming. However, there are still several big topics to tackle, including encapsulation
and polymorphism. Rather than trying to cover such big topics inside this already
lengthy chapter, I have decided to break things up and give them their own chapters
where they can have plenty of room to unfold naturally.
Plenty of material was covered in this chapter, including inheritance, virtual
methods, aggregation, and form inheritance. If you are new to this material, it would
only be natural that some of it did not sink in the first time around. If so, you
can either re-read the chapter or go on to the next chapter and see if some of it
doesn't start to become clear when you work with new examples that approach the material
from a slightly different angle.
Inheritance and virtual methods are everywhere in the BCB, and the more you work
with them, the easier it will be to understand the principles involved. However,
this is material that you must master if you want to be good at using BCB--and particularly
if you want to start creating your own components.
|